//
//  GDImage.m
//  GDFramework
//
//  Created by David Thorpe on 2007-07-20.
//  Copyright 2007 David Thorpe. All rights reserved.
//
//  Modified by David Rudie on 2009-05-09.
//  Modified and added functions by David Rudie are under the
//  following license:
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

#import <gd.h>
#import <GDFramework/GDFramework.h>


@interface GDImage (Private)
- (gdImagePtr)imagePtr;
- (void)setImagePtr:(gdImagePtr)imagePtr;
@end

@interface GDColor (Private)
- (id)initWithPtr:(void *)theImagePtr index:(NSInteger)theIndex;
@end

@interface GDFont (Private)
- (NSString *)path;
- (gdFontPtr)fontPtr;
- (CGFloat)points;
@end

@implementation GDImage

+ (BOOL)data:(NSData *)theData matches:(unsigned const char *)theSignature offset:(NSUInteger)theOffset length:(NSUInteger)theLength {
	unsigned const char *theBytes = [theData bytes];
	if ([theData length] < (theOffset + theLength))
		return NO;
	for (unsigned i = 0; i < theLength; i++) {
		if (*(theBytes + theOffset + i) != *(theSignature + i))
			return NO;
	}
	return YES;
}

+ (BOOL)dataIsPNG:(NSData *)theData {
	// starts with 0x89504e470d0a1a0a
	unsigned const char *theSignature = (unsigned const char *)"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a";
	return [self data:theData matches:theSignature offset:0 length:8];
}

+ (BOOL)dataIsJPEG:(NSData *)theData {
	// starts with 0xFFD8FF, 0x4A464946 at byte 6 onwards or 0x45786966 at byte 6 onwards
	unsigned const char *theSignature1 = (unsigned const char *)"\xFF\xD8\xFF";
	unsigned const char *theSignature2 = (unsigned const char *)"\x4A\x46\x49\x46";
	unsigned const char *theSignature3 = (unsigned const char *)"\x45\x78\x69\x66";

	if ([self data:theData matches:theSignature1 offset:0 length:3] == NO)
		return NO;
	if ([self data:theData matches:theSignature2 offset:6 length:4] == YES)
		return YES;
	if ([self data:theData matches:theSignature3 offset:6 length:4] == YES)
		return YES;
	return NO;
}

+ (BOOL)dataIsGIF:(NSData *)theData {
	// data starts with 0x474946383961 or 0x474946383761
	unsigned const char *theSignature1 = (unsigned const char *)"\x47\x49\x46\x38\x39\x61";
	unsigned const char *theSignature2 = (unsigned const char *)"\x47\x49\x46\x38\x37\x61";

	if ([self data:theData matches:theSignature1 offset:0 length:6] == YES)
		return YES;
	if ([self data:theData matches:theSignature2 offset:0 length:6] == YES)
		return YES;
	return NO;
}

+ (BOOL)dataIsBMP:(NSData *)theData {
	// data starts with 0x424D
	unsigned const char *theSignature = (unsigned const char *)"\x42\x4D";

	if ([self data:theData matches:theSignature offset:0 length:2] == YES)
		return YES;
	return NO;
}

+ (BOOL)dataIsTIFF:(NSData *)theData {
	// data starts with 0x4D4D or 0x4949
	unsigned const char *theSignature1 = (unsigned const char *)"\x4D\x4D";
	unsigned const char *theSignature2 = (unsigned const char *)"\x49\x49";

	if ([self data:theData matches:theSignature1 offset:0 length:2] == YES)
		return YES;
	if ([self data:theData matches:theSignature2 offset:0 length:2] == YES)
		return YES;
	return NO;
}

+ (GDImageType)typeForFilename:(NSString *)thePath {
	NSString *theExtension = [thePath pathExtension];
	if ([theExtension length] == 0)
		return GDImageTypeUnknown;
	if ([theExtension compare:@"jpeg" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeJPEG;
	if ([theExtension compare:@"jpg" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeJPEG;
	if ([theExtension compare:@"jpe" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeJPEG;
	if ([theExtension compare:@"gif" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeGIF;
	if ([theExtension compare:@"png" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypePNG;
	if ([theExtension compare:@"bmp" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeBMP;
	if ([theExtension compare:@"tif" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeTIFF;
	if ([theExtension compare:@"tiff" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeTIFF;
	return GDImageTypeUnknown;
}

+ (GDImageType)typeForMimetype:(NSString *)theMimetype {
	if ([theMimetype length] == 0)
		return GDImageTypeUnknown;
	if ([theMimetype compare:@"image/jpg" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeJPEG;
	if ([theMimetype compare:@"image/jpeg" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeJPEG;
	if ([theMimetype compare:@"image/pjpeg" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeJPEG;
	if ([theMimetype compare:@"image/gif" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeGIF;
	if ([theMimetype compare:@"image/png" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypePNG;
	if ([theMimetype compare:@"image/x-ms-bmp" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeBMP;
	if ([theMimetype compare:@"image/bmp" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeBMP;
	if ([theMimetype compare:@"image/tiff" options:NSCaseInsensitiveSearch] == NSOrderedSame)
		return GDImageTypeTIFF;
	return GDImageTypeUnknown;
}

+ (GDImageType)typeForData:(NSData *)theData {
	if ([self dataIsJPEG:theData])
		return GDImageTypeJPEG;
	if ([self dataIsGIF:theData])
		return GDImageTypeGIF;
	if ([self dataIsPNG:theData])
		return GDImageTypePNG;
	if ([self dataIsBMP:theData])
		return GDImageTypeBMP;
	if ([self dataIsTIFF:theData])
		return GDImageTypeTIFF;
	return GDImageTypeUnknown;
}

+ (GDImageType)typeForFile:(NSString *)thePath {
	// read first 16 bytes of the file to determine signature
	NSFileHandle *theFileHandle = [NSFileHandle fileHandleForReadingAtPath:thePath];
	if (theFileHandle == nil)
		return GDImageTypeUnknown;
	NSData *theData = [theFileHandle readDataOfLength:16];
	[theFileHandle closeFile];
	if ([theData length] == 0)
		return GDImageTypeUnknown;
	return [self typeForData:theData];
}

// create an empty image
- (id)initWithSize:(NSSize)theSize {
	self = [super init];
	if (self) {
		m_theImagePtr = gdImageCreate(theSize.width, theSize.height);
		if (m_theImagePtr == nil) {
			[self release];
			return nil;
		}
	}
	return self;
}

// create an image from pointer
- (id)initWithPtr:(void *)theImagePtr {
	self = [super init];
	if (self) {
		m_theImagePtr = theImagePtr;
		if (m_theImagePtr == nil) {
			[self release];
			return nil;
		}
	}
	return self;
}

// create image from data and auto-detect format
- (id)initWithData:(NSData *)theData {
	self = [super init];
	if (self) {
		m_theImagePtr = nil;
		switch ([GDImage typeForData:theData]) {
			case GDImageTypeJPEG:
				m_theImagePtr = gdImageCreateFromJpegPtr([theData length], (void *)[theData bytes]);
				break;
			case GDImageTypeGIF:
				m_theImagePtr = gdImageCreateFromGifPtr([theData length], (void *)[theData bytes]);
				break;
			case GDImageTypePNG:
				m_theImagePtr = gdImageCreateFromPngPtr([theData length], (void *)[theData bytes]);
				break;
			default:
				break;
		}
		if (m_theImagePtr == nil) {
			[self release];
			return nil;
		}
	}
	return nil;
}

// create image from file and auto-detect format
- (id)initWithFile:(NSString *)thePath {
	self = [super init];
	if (self) {
		m_theImagePtr = nil;
		GDImageType theType = [GDImage typeForFile:thePath];
		if (theType == GDImageTypeUnknown) {
			[self release];
			return nil;
		}
		NSData *theData = [NSData dataWithContentsOfFile:thePath];
		if (theData == nil) {
			[self release];
			return nil;
		}
		switch ([GDImage typeForFile:thePath]) {
			case GDImageTypeJPEG:
				m_theImagePtr = gdImageCreateFromJpegPtr([theData length], (void *)[theData bytes]);
				break;
			case GDImageTypeGIF:
				m_theImagePtr = gdImageCreateFromGifPtr([theData length], (void *)[theData bytes]);
				break;
			case GDImageTypePNG:
				m_theImagePtr = gdImageCreateFromPngPtr([theData length], (void *)[theData bytes]);
				break;
			default:
				break;
		}
		if (m_theImagePtr == nil) {
			[self release];
			return nil;
		}
	}
	return self;
}

- (void)dealloc {
	[self setImagePtr:nil];
	[super dealloc];
}

- (gdImagePtr)imagePtr {
	return (gdImagePtr)m_theImagePtr;
}

- (void)setImagePtr:(gdImagePtr)imagePtr {
	if (m_theImagePtr) {
		gdImageDestroy(m_theImagePtr);
	}
	m_theImagePtr = imagePtr;
}

- (NSData *)_dataForType:(GDImageType)theImageType quality:(NSInteger)theQuality {
	NSInteger theJPEGQuality = (theQuality > 95 ? 95 : theQuality);
	int theLength = 0;
	void *theBytes = nil;
	switch (theImageType) {
		case GDImageTypeJPEG:
			theBytes = gdImageJpegPtr([self imagePtr], &theLength, theJPEGQuality);
			break;
		case GDImageTypeGIF:
			theBytes = gdImageGifPtr([self imagePtr], &theLength);
			break;
		case GDImageTypePNG:
			theBytes = gdImagePngPtrEx([self imagePtr], &theLength, -1);
			break;
		default:
			break;
	}
	if (theBytes == nil) {
		return nil;
	}
	NSData *theData = [NSData dataWithBytes:theBytes length:theLength];
	gdFree(theBytes);
	return theData;
}

- (BOOL)_writeToFile:(NSString *)thePath type:(GDImageType)theImageType quality:(NSInteger)theQuality {
// !!!: We are not checking if the file exists so that it may overwrite the file by default.
// !!!: If we do check, then it will just write data into the file that already exists.
//	if ([[NSFileManager defaultManager] fileExistsAtPath:thePath] == NO) {
		if ([[NSFileManager defaultManager] createFileAtPath:thePath contents:nil attributes:nil] == NO) {
			return NO;
		}
//	}
	NSData *theData = [self _dataForType:theImageType quality:theQuality];
	if (theData == nil) {
		return NO;
	}
	NSFileHandle *theFileHandle = [NSFileHandle fileHandleForWritingAtPath:thePath];
	if (theFileHandle == nil) {
		return NO;
	}
	[theFileHandle writeData:theData];
	[theFileHandle closeFile];
	return YES;
}

- (NSData *)dataForType:(GDImageType)theImageType {
	return [self _dataForType:theImageType quality:-1];
}

- (NSData *)dataForType:(GDImageType)theImageType quality:(CGFloat)theQuality {
	return [self _dataForType:theImageType quality:(NSInteger)(theQuality * 95.0)];
}

- (BOOL)writeToFile:(NSString *)thePath type:(GDImageType)theImageType quality:(CGFloat)theQuality {
	return [self _writeToFile:thePath type:theImageType quality:(NSInteger)(theQuality * 95.0)];
}

- (BOOL)writeToFile:(NSString *)thePath type:(GDImageType)theImageType {
	return [self _writeToFile:thePath type:theImageType quality:-1];
}

- (CGFloat)width {
	return (CGFloat)gdImageSX([self imagePtr]);
}

- (CGFloat)height {
	return (CGFloat)gdImageSY([self imagePtr]);
}

- (NSSize)size {
	return NSMakeSize([self width], [self  height]);
}

- (GDImage *)imageResizedTo:(NSSize)theSize resampled:(BOOL)isResampled {
	// create a new version of the image in the new size
	gdImagePtr theNewImage = gdImageCreateTrueColor(theSize.width, theSize.height);
	if (theNewImage == nil) {
		return nil;
	}
	// copy the exiting image into the new one
	if (isResampled) {
		gdImageCopyResized(theNewImage, [self imagePtr], 0, 0, 0, 0, gdImageSX(theNewImage),
						   gdImageSY(theNewImage), gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	} else {
		gdImageCopyResampled(theNewImage, [self imagePtr], 0, 0, 0, 0, gdImageSX(theNewImage),
							 gdImageSY(theNewImage), gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	}
	// return image
	return [[[GDImage alloc] initWithPtr:theNewImage] autorelease];
}

- (GDImage *)imageConstrainedToMinSize:(NSSize)theSize resampled:(BOOL)isResampled {
	// determine what the size of the new image should be
	float widthFactor = (theSize.width / [self width]);
	float heightFactor = (theSize.height / [self height]);
	gdImagePtr theNewImage = nil;
	if (([self height] * widthFactor) > theSize.height) {
		// use width factor
		theNewImage = gdImageCreateTrueColor(theSize.width, [self height] * widthFactor);
	} else {
		// use height factor
		theNewImage = gdImageCreateTrueColor([self width] * heightFactor, theSize.height);
	}
	// create a new version of the image in the new size
	if (theNewImage == nil) {
		return nil;
	}
	// copy the existing image into the new one
	if (isResampled) {
		gdImageCopyResized(theNewImage, [self imagePtr], 0, 0, 0, 0, gdImageSX(theNewImage),
						   gdImageSY(theNewImage), gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	} else {
		gdImageCopyResampled(theNewImage, [self imagePtr], 0, 0, 0, 0, gdImageSX(theNewImage),
							 gdImageSY(theNewImage), gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	}
	// return image
	return [[[GDImage alloc] initWithPtr:theNewImage] autorelease];
}

- (GDImage *)imageConstrainedToMaxSize:(NSSize)theSize resampled:(BOOL)isResampled {
	// determine what the size of the new image should be
	CGFloat widthFactor = (theSize.width / [self width]);
	CGFloat heightFactor = (theSize.height / [self height]);
	gdImagePtr theNewImage = nil;
	if (([self height] * widthFactor) < theSize.height) {
		// use width factor
		theNewImage = gdImageCreateTrueColor([self width] * widthFactor, [self height] * widthFactor);
	} else {
		// use height factor
		theNewImage = gdImageCreateTrueColor([self width] * heightFactor, [self height] * heightFactor);
	}
	// create a new version of the image in the new size
	if (theNewImage == nil) {
		return nil;
	}
	// copy the existing image into the new one
	if (isResampled) {
		gdImageCopyResized(theNewImage, [self imagePtr], 0, 0, 0, 0, gdImageSX(theNewImage),
						   gdImageSY(theNewImage), gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	} else {
		gdImageCopyResampled(theNewImage, [self imagePtr], 0, 0, 0, 0, gdImageSX(theNewImage),
							 gdImageSY(theNewImage), gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	}
	// return image
	return [[[GDImage alloc] initWithPtr:theNewImage] autorelease];
}

- (GDImage *)imageConstrainedToSize:(NSSize)theSize resampled:(BOOL)isResampled cropped:(NSInteger)flags {
	// determine what the size of the new image should be
	CGFloat widthFactor = (theSize.width / [self width]);
	CGFloat heightFactor = (theSize.height / [self height]);
	CGFloat factor;
	CGFloat newWidth, newHeight;

	// round resolution -- David Rudie
	theSize.width = floor(theSize.width + 0.6);
	theSize.height = floor(theSize.height + 0.6);

	if (([self height] * widthFactor) > theSize.height) {
		// use width factor
		newWidth = theSize.width;
		newHeight = [self height] * widthFactor;
		factor = widthFactor;
	} else {
		// use height factor
		newWidth = [self width] * heightFactor;
		newHeight = theSize.height;
		factor = heightFactor;
	}
	// determine source offset
	CGFloat sourceX = 0;
	CGFloat sourceY = 0;
	if (newWidth != theSize.width) {
		if (flags & GDCropLeft) {
			// left crop
			sourceX = 0.0;
		} else if (flags & GDCropRight) {
			// right crop
			sourceX = (newWidth - theSize.width) / factor;
		} else {
			// center crop
			sourceX = (newWidth - theSize.width) / (factor * 2.0);
		}
	}
	if (newHeight != theSize.height) {
		if (flags & GDCropTop) {
			// top crop
			sourceY = 0.0;
		} else if (flags & GDCropBottom) {
			// bottom crop
			sourceY = (newHeight - theSize.height) / factor;
		} else {
			// center crop
			sourceY = (newHeight - theSize.height) / (factor * 2.0);
		}
	}
	// create a new version of the image in the new size
	gdImagePtr theNewImage = gdImageCreateTrueColor(theSize.width, theSize.height);
	if (theNewImage == nil) {
		return nil;
	}

	// Added by David Rudie
	gdImageAlphaBlending(theNewImage, 0);
	gdImageSaveAlpha(theNewImage, 1);

	// copy the exiting image into the new one
	if (isResampled) {
		gdImageCopyResized(theNewImage, [self imagePtr], 0, 0, sourceX, sourceY,
						   newWidth, newHeight, gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	} else {
		gdImageCopyResampled(theNewImage, [self imagePtr], 0, 0, sourceX, sourceY,
							 newWidth, newHeight, gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));
	}
	// return image
	return [[[GDImage alloc] initWithPtr:theNewImage] autorelease];
}

- (GDImage *)imageCroppedTo:(NSRect)theRect resampled:(BOOL)isResampled {
	// create a new version of the image in the new size
	gdImagePtr theNewImage = gdImageCreateTrueColor(theRect.size.width, theRect.size.height);
	if (theNewImage == nil) {
		return nil;
	}
	// convert rect co-ordinates to GD co-ordinates
	NSInteger left = theRect.origin.x;
	NSInteger top = [self height] - (theRect.origin.y + theRect.size.height);
	// copy the existing image into the new one
	if (isResampled) {
		gdImageCopyResized(theNewImage, [self imagePtr], 0, 0, left, top, gdImageSX(theNewImage),
						   gdImageSY(theNewImage), theRect.size.width, theRect.size.height);
	} else {
		gdImageCopyResampled(theNewImage, [self imagePtr], 0, 0, left, top, gdImageSX(theNewImage),
							 gdImageSY(theNewImage), theRect.size.width, theRect.size.height);
	}
	// return image
	return [[[GDImage alloc] initWithPtr:theNewImage] autorelease];
}

- (GDColor *)colorForPoint:(NSPoint)thePoint {
	NSInteger theColorIndex = gdImageGetPixel([self imagePtr], thePoint.x, ([self height] - thePoint.y));
	return (theColorIndex < 0) ? nil : [[[GDColor alloc] initWithPtr:[self imagePtr] index:theColorIndex] autorelease];
}

- (GDColor *)colorForRed:(CGFloat)theRed green:(CGFloat)theGreen blue:(CGFloat)theBlue {
	NSInteger theColorIndex = gdImageColorResolve([self imagePtr], (NSInteger)(theRed * 255.0),
												  (NSInteger)(theGreen * 255.0), (NSInteger)(theBlue * 255.0));
	return (theColorIndex < 0) ? nil : [[[GDColor alloc] initWithPtr:[self imagePtr] index:theColorIndex] autorelease];
}

- (GDColor *)colorForRed:(CGFloat)theRed green:(CGFloat)theGreen blue:(CGFloat)theBlue alpha:(CGFloat)theAlpha {
	NSInteger theColorIndex = gdImageColorResolveAlpha([self imagePtr], (NSInteger)(theRed * 255.0),
												 (NSInteger)(theGreen * 255.0), (NSInteger)(theBlue * 255.0),
												 (NSInteger)((1.0 - theAlpha) * 127.0));
	return (theColorIndex < 0) ? nil : [[[GDColor alloc] initWithPtr:[self imagePtr] index:theColorIndex] autorelease];
}

- (void)drawRectFill:(NSRect)theRect withColor:(GDColor *)theColor {
	// convert rect co-ordinates to GD co-ordinates
	NSInteger left = theRect.origin.x;
	NSInteger right = theRect.origin.x + theRect.size.width;
	NSInteger top = [self height] - (theRect.origin.y + theRect.size.height);
	NSInteger bottom = [self height] - theRect.origin.y;
	// draw
	gdImageFilledRectangle([self imagePtr], left, top, right, bottom, [theColor index]);
}

- (void)drawRectOutline:(NSRect)theRect withColor:(GDColor *)theColor {
	// convert rect co-ordinates to GD co-ordinates
	NSInteger left = theRect.origin.x;
	NSInteger right = theRect.origin.x + theRect.size.width;
	NSInteger top = [self height] - (theRect.origin.y + theRect.size.height);
	NSInteger bottom = [self height] - theRect.origin.y;
	// draw
	gdImageRectangle([self imagePtr], left, top, right, bottom, [theColor index]);
}

- (void)drawLineFrom:(NSPoint)theSource to:(NSPoint)theDest withColor:(GDColor *)theColor {
	// convert points to GD units
	NSInteger x1 = theSource.x;
	NSInteger y1 = [self height] - theSource.y;
	NSInteger x2 = theDest.x;
	NSInteger y2 = [self height] - theDest.y;
	// draw
	gdImageLine([self imagePtr], x1, y1, x2, y2, [theColor index]);
}

- (void)drawImage:(GDImage *)theImage intoRect:(NSRect)theRect {
	// convert rect co-ordinates to GD co-ordinates
	NSInteger left = theRect.origin.x;
	NSInteger top = [self height] - (theRect.origin.y + theRect.size.height);
	// draw
	gdImageCopyResampled([self imagePtr], [theImage imagePtr], left, top, 0, 0,
						 theRect.size.width, theRect.size.height, [theImage width], [theImage height]);
}

- (void)filterSharpenBy:(CGFloat)theAmount {
	gdImageSharpen([self imagePtr], theAmount * 100.0);
}

- (void)drawString:(NSString *)theString point:(NSPoint)thePoint font:(GDFont *)theFont color:(GDColor *)theColor {
	// different versions for truetype and system fonts
	NSInteger x = thePoint.x;
	NSInteger y = ([self height] - thePoint.y);
	int bounds[8];
	if ([theFont fontPtr]) {
		gdImageString([self imagePtr], [theFont fontPtr], x, y, (unsigned char *)[theString UTF8String],
					  [theColor index]);
	} else {
		gdImageStringFT([self imagePtr], &bounds[0], [theColor index], (char *)[[theFont path] UTF8String],
						[theFont points], 0.0, x, y, (char *)[theString UTF8String]);
	}
}

// The following functions are Copyright 2009 David Rudie
- (GDImage *)imageSubpixelConstrainedToSize:(NSSize)theSize cropped:(NSInteger)flags cropRatio:(NSNumber *)theCropRatio {
	CGFloat widthFactor = (theSize.width / [self width]);
	CGFloat heightFactor = (theSize.height / [self height]);
	CGFloat factor;
	CGFloat newWidth, newHeight;

	// round resolution
	theSize.width = floor(theSize.width + 0.6);
	theSize.height = floor(theSize.height + 0.6);

	if (([self height] * widthFactor) > theSize.height) {
		newWidth = theSize.width;
		newHeight = [self height] * widthFactor;
		factor = widthFactor;
	} else {
		newWidth = [self width] * heightFactor;
		newHeight = theSize.height;
		factor = heightFactor;
	}

	CGFloat sourceX = 0;
	CGFloat sourceY = 0;
	if (newWidth != theSize.width) {
		if (flags & GDCropLeft) {
			sourceX = 0.0;
		} else if (flags & GDCropRight) {
			sourceX = (newWidth - theSize.width) / factor;
		} else {
			sourceX = (newWidth - theSize.width) / (factor * 2.0);
		}
	}
	if (newHeight != theSize.height) {
		if (flags & GDCropTop) {
			sourceY = 0.0;
		} else if (flags & GDCropBottom) {
			sourceY = (newHeight - theSize.height) / factor;
		} else {
			sourceY = (newHeight - theSize.height) / (factor * 2.0);
		}
	}

	CGFloat imageWidth = 0.0;
	CGFloat sourceAspectRatio = [self width] / [self height];
	if ([theCropRatio doubleValue] <= 0.0)
		imageWidth = [self width];
	else if ([theCropRatio doubleValue] <= sourceAspectRatio)
		imageWidth = [self height] * [theCropRatio doubleValue];
	else if ([theCropRatio doubleValue] > sourceAspectRatio)
		imageWidth = [self width];

	gdImagePtr theNewImage = gdImageCreateTrueColor(imageWidth, theSize.height);
	if (theNewImage == nil)
		return nil;
	gdImageAlphaBlending(theNewImage, 0);
	gdImageSaveAlpha(theNewImage, 1);

	gdImagePtr theNewImage2 = gdImageCreateTrueColor(theSize.width, theSize.height);
	if (theNewImage2 == nil)
		return nil;
	gdImageAlphaBlending(theNewImage2, 0);
	gdImageSaveAlpha(theNewImage2, 1);

	gdImageCopyResampled(theNewImage, [self imagePtr], 0, 0, sourceX, sourceY,
						 [self width], newHeight, gdImageSX([self imagePtr]), gdImageSY([self imagePtr]));

	NSInteger fullWidth;
	NSInteger colorPoint[5];

	CGFloat colorRed, colorGreen, colorBlue, colorAlpha;

	NSInteger x, y;
	for (y = 0; y < gdImageSY(theNewImage); y++) {
		for (x = 0; x < gdImageSX(theNewImage2); x++) {
			fullWidth = x * (imageWidth / theSize.width) + 2;

			colorPoint[0] = gdImageGetPixel(theNewImage,
											fullWidth - 2,
											y);
			colorPoint[1] = gdImageGetPixel(theNewImage,
											fullWidth - 1,
											y);
			colorPoint[2] = gdImageGetPixel(theNewImage,
											(fullWidth >= ([self width] - 1)) ? [self width] - 1 : fullWidth,
											y);
			colorPoint[3] = gdImageGetPixel(theNewImage,
											((fullWidth + 1) >= ([self width] - 1)) ? [self width] - 1 : fullWidth + 1,
											y);
			colorPoint[4] = gdImageGetPixel(theNewImage,
											((fullWidth + 2) >= ([self width] - 1)) ? [self width] - 1 : fullWidth + 2,
											y);

			colorRed = (((CGFloat)gdImageRed(theNewImage, colorPoint[0]) * 0.5) +
						((CGFloat)gdImageRed(theNewImage, colorPoint[1]) * 0.5) +
						((CGFloat)gdImageRed(theNewImage, colorPoint[2]) / 0.5)) / 3;
			colorRed = [self clampColor:colorRed];

			colorGreen = (((CGFloat)gdImageGreen(theNewImage, colorPoint[1]) * 0.5) +
						  ((CGFloat)gdImageGreen(theNewImage, colorPoint[2]) / 0.5) +
						  ((CGFloat)gdImageGreen(theNewImage, colorPoint[3]) * 0.5)) / 3;
			colorGreen = [self clampColor:colorGreen];

			colorBlue = (((CGFloat)gdImageBlue(theNewImage, colorPoint[2]) / 0.5) +
						 ((CGFloat)gdImageBlue(theNewImage, colorPoint[3]) * 0.5) +
						 ((CGFloat)gdImageBlue(theNewImage, colorPoint[4]) * 0.5)) / 3;
			colorBlue = [self clampColor:colorBlue];

			colorAlpha = ((CGFloat)gdImageAlpha(theNewImage, colorPoint[0]) +
						  (CGFloat)gdImageAlpha(theNewImage, colorPoint[1]) +
						  (CGFloat)gdImageAlpha(theNewImage, colorPoint[2]) +
						  (CGFloat)gdImageAlpha(theNewImage, colorPoint[3]) +
						  (CGFloat)gdImageAlpha(theNewImage, colorPoint[4])) / 5;
			colorAlpha += 128.0;
			colorAlpha = [self clampColor:colorAlpha];

			gdImageSetPixel(theNewImage2, x, y, gdImageColorAllocateAlpha(theNewImage2, colorRed, colorGreen, colorBlue, colorAlpha));
		}
	}

	return [[[GDImage alloc] initWithPtr:theNewImage2] autorelease];
}

- (CGFloat)clampColor:(CGFloat)theColor {
	theColor = theColor <   0 ?   0 : theColor;
	theColor = theColor > 255 ? 255 : theColor;

	return theColor;
}

- (void)mergeImage:(GDImage *)theImage textPosition:(NSInteger)theTextPosition sourceYPoint:(NSInteger)theSourceYPoint width:(CGFloat)theWidth {
	gdImageCopyMerge([self imagePtr], [theImage imagePtr], 0, theTextPosition, 0, theSourceYPoint, theWidth, 12, 65);
}

@end
